Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 22 章 删除数据

作者:Adam Freeman
翻译:陈广
日期:2019-6-2


删除数据可能是一个令人惊讶的复杂任务,尤其是涉及处理关联数据或建模现有数据库时。在本章中,我描述了 Entity Framework Core 处理删除数据的功能,演示了它们的工作原理,并解释了它们何时有用。表22-1为本章简述。

表 22-1:高级删除功能简述

问题 回答
它们是什么? 这些功能允许您指定数据库服务器如何响应删除数据的请求。
它们有何用途? 当一个对象被删除时,有几种不同的处理关联数据的方法,选择正确的方法将确保应用程序能够访问它需要的数据。
如何使用它们 这些功能通过组合 Fluent API 语句和对项目中的存储库类的更改来应用。
是否有任何缺陷或限制? 必须注意,不要在数据库中造成意外的级联删除,并删除比预期更多的数据。同样,删除不充分也可能导致孤立的数据。
有没有其他选择? 您可以依赖 Entity Framework Core 应用于数据库的默认行为。

表 22-2 为本章摘要。

表21-2:本章摘要

问题 解决方案 清单
更改删除行为 使用OnDelete方法 1-19

准备本章

我继教使用 AdvancedApp 项目,但为准备本章,需要做一些改变。为显示SecondaryIdentity对象的详细信息,我在 Views/Home 文件夹下创建了一个名为 SecondaryIdentities.cshtml 的文件,并添加清单22-1所示的内容以创建一个分部视图。

提示:如果您不想跟随构建示例项目的过程,可以从本书的源代码库下载所有所需的文件,这些文件可在 https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc 上找到。

清单 22-1:Views/Home 文件夹下的 SecondaryIdentities.cshtml 文件的内容

<h3 class="bg-info p-2 text-center text-white">Secondary Identities</h3>
<table class="table table-sm table-striped">
    <thead>
        <tr>
            <th>Key</th>
            <th>Name</th>
            <th>Active Use</th>
            <th>Foreign SSN</th>
            <th>Foreign FamilyName</th>
            <th>Foreign FirstName</th>
        </tr>
    </thead>
    <tbody>
        <tr class="placeholder"><td colspan="6" class="text-center">No Data</td></tr>
        @foreach (SecondaryIdentity ident in ViewBag.Secondaries)
        {
            <tr>
                <td>@ident.Id</td>
                <td>@ident.Name</td>
                <td>@ident.InActiveUse</td>
                <td>@(ident.PrimarySSN ?? "(null)")</td>
                <td>@(ident.PrimaryFirstName ?? "(null)")</td>
                <td>@(ident.PrimaryFamilyName ?? "(null)")</td>
            </tr>
        }
    </tbody>
</table>

为了将分部视图合并到显示给用户,我将清单22-2中所示的元素添加到 Home 控制器使用的 Index 视图中。我还从显示雇员对象的表中移除了In UseLast Updated列。

清单 22-2:Views/Home 文件夹下的 Index.cshtml 文件,添加分部视图

@model IEnumerable<Employee>
@{
    ViewData["Title"] = "Advanced Features";
    Layout = "_Layout";
}
<h3 class="bg-info p-2 text-center text-white">Employees</h3>
<table class="table table-sm table-striped">
    <thead>
        <tr>
            <th>SSN</th>
            <th>First Name</th>
            <th>Family Name</th>
            <th>Salary</th>
            <th>Other Name</th>
            @*<th>In Use</th>*@
            @*<th>Last Updated</th>*@
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr class="placeholder"><td colspan="7" class="text-center">No Data</td></tr>
        @foreach (Employee e in Model)
        {
    <tr>
        <td>@e.SSN</td>
        <td>@e.FirstName</td>
        <td>@e.FamilyName</td>
        <td>@e.Salary</td>
        <td>@(e.OtherIdentity?.Name ?? "(None)")</td>
        @*<td>@(e.OtherIdentity?.InActiveUse.ToString() ?? "(N/A)")</td>*@
        @*<td>@e.LastUpdated.ToLocalTime()</td>*@
        <td class="text-right">
            <form>
                <input type="hidden" name="SSN" value="@e.SSN" />
                <input type="hidden" name="Firstname" value="@e.FirstName" />
                <input type="hidden" name="FamilyName"
                       value="@e.FamilyName" />
                <input type="hidden" name="RowVersion"
                       asp-for="@e.RowVersion" />
                <button type="submit" asp-action="Delete" formmethod="post"
                        class="btn btn-sm btn-danger">
                    Delete
                </button>
                <button type="submit" asp-action="Edit" formmethod="get"
                        class="btn btn-sm btn-primary">
                    Edit
                </button>
            </form>
        </td>
    </tr>
        }
    </tbody>
</table>
@await  Html.PartialAsync("SecondaryIdentities")
<div class="text-center">
    <a asp-action="Edit" class="btn btn-primary">Create</a>
</div>

对于 Home 控制器,我更改了Index action 以提供通过ViewBagSecondaryIdentity的访问,如清单22-3所示。我还更改了Delete方法以移除软删除功能,并执行一个常规删除,类似于前面章节中所使用的。(我在本章末尾恢复了软删除功能。)

清单 22-3:Controllers 文件夹下的 HomeController.cs 文件,修改代码

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System;

namespace AdvancedApp.Controllers
{
    public class HomeController : Controller
    {
        private AdvancedContext context;
        public HomeController(AdvancedContext ctx) => context = ctx;

        public IActionResult Index()
        {
            ViewBag.Secondaries = context.Set<SecondaryIdentity>();
            return View(context.Employees.Include(e => e.OtherIdentity)
                .OrderByDescending(e => EF.Property<DateTime>(e, "LastUpdated")));
        }

        public IActionResult Edit(string SSN, string firstName, string familyName)
        {
            return View(string.IsNullOrWhiteSpace(SSN)
                ? new Employee() : context.Employees.Include(e => e.OtherIdentity)
                    .First(e => e.SSN == SSN
                        && e.FirstName == firstName
                        && e.FamilyName == familyName));
        }

        [HttpPost]
        public IActionResult Update(Employee employee)
        {
            if (context.Employees.Count(e => e.SSN == employee.SSN
            && e.FirstName == employee.FirstName
            && e.FamilyName == employee.FamilyName) == 0)
            {
                context.Add(employee);
            }
            else
            {
                Employee e = new Employee
                {
                    SSN = employee.SSN,
                    FirstName = employee.FirstName,
                    FamilyName = employee.FamilyName,
                    RowVersion = employee.RowVersion
                };
                context.Employees.Attach(e);
                e.Salary = employee.Salary;
                e.LastUpdated = DateTime.Now;
            }
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }

        [HttpPost]
        public IActionResult Delete(Employee employee)
        {
            context.Remove(employee);
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }
    }
}

接下来在 AdvancedApp 项目文件夹下运行清单 22-4 所示的命令,删除并重建数据库。

清单 22-4:删除并重建数据库

dotnet ef database drop --force
dotnet ef database update

使用dotnet run启动应用程序,导航至 http://localhost:5000,单击【Create】按钮,使用表22-3中的值存储四个Employee对象。

表 22-3:创建示例对象所需的数据值

SSN FirstName FamilyName Salary Other Name In Active Use
420-39-1864 Bob Smith 100000 Robert Checked
657-03-5898 Alice Jones 200000 Allie Checked
300-30-0522 Peter Davies 180000 Pette Checked
751-07-9405 Theodora Francis 140000 Dora Checked

当您创建了所有四个对象,将看到图22-1所示的布局。

图22-1 运行示例应用程序

理解删除约束

context 的Remove方法用于从数据库中删除数据。传递一个对象给这个方法告诉 Entity Framework Core 不再需要数据库中相应的数据,当SaveChanges方法被调用时,DELETE命令将被发送至数据库服务器。

当要删除的对象与存储在数据库中的其他数据有关系时,ASP.NET Core MVC 应用程序中就会出现一个常见的问题。单击应用程序显示的某个Employee对象的【Delete】按钮可以看到一个示例。数据库不允许删除数据,您将看到图22-2所示的异常。

图22-2 试图删除数据

在删除关系中的主要实体时,根据数据模型配置,依赖实体将发生三种情况之一。

  • 依赖实体将被删除。这意味着删除一个Employee对象,与之关联的SecondaryIdentity也将被删除。
  • 依赖实体的外键属性被设置为null。这意味着删除一个Employee对象将导致关联的SecondaryIdentity对象的SSNPrimaryFirstNamePrimaryFamily属性被设置为null
  • 依赖属性没有改变。

第三个选项实际上意味着您必须管理应用程序中的关联数据,负责确保不尝试任何将删除关系所依赖数据的操作。数据库服务器努力确保其管理的数据库的完整性,并将报告错误,而不是允许创建相关问题。

当关系定义之后,Entity Framework Core 会遵循一系列约定来选择删除行为,使用由DeleteBehavior枚举定义的值之一,如表22-4所述。

表 22-4:DeleteBehavior 值

名称 描述
Cascade 依赖实体将与主要实体一起自动删除。
SetNull 依赖实体的主键被数据库服务器设置为空。
ClientSetNull 依赖实体的主键被 Entity Framework Core 设置为空
Restrict 依赖实体没有改变

EmployeeSecondaryIdentity类之间的关系在CompositeKey迁移中配置。如果您检查 Migrations 文件夹下的<timestamp>_CompositeKey.cs文件中的Up方法,将看到设置删除行为的语句。

migrationBuilder.CreateTable(
    name: "SecondaryIdentity",
    columns: table => new
    {
        Id = table.Column<long>(nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Name = table.Column<string>(nullable: true),
        InActiveUse = table.Column<bool>(nullable: false),
        PrimarySSN = table.Column<string>(nullable: true),
        PrimaryFamilyName = table.Column<string>(nullable: true),
        PrimaryFirstName = table.Column<string>(nullable: true)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_SecondaryIdentity", x => x.Id);
        table.ForeignKey(
            name: "FK_SecondaryIdentity_Employees_PrimarySSN_PrimaryFirstName_PrimaryFamilyName",
            columns: x => new { x.PrimarySSN, x.PrimaryFirstName, x.PrimaryFamilyName },
            principalTable: "Employees",
            principalColumns: new[] { "SSN", "FirstName", "FamilyName" },
            onDelete: ReferentialAction.Restrict);
    });

此关系已被配置为Restrict行为。当关联的Employee被删除时,SecondaryIdentity对象没有改变,导致了图22-2所示的异常。

配置删除行为

获取所需行为的最可靠方法是使用 Fleunt API 选择所需的删除行为,使用DeleteBehavior值显式地配置数据模型。表中的描述似乎很简单,但也有一些复杂之处。这些行为根据需求及可选关系可能会有所不同。此外,为了使事情变得更加困难,Entity Framework Core 将只对从数据库加载的关联数据执行一些操作,这在 ASP.NET Core MVC 应用程序中可能会引起混淆,在这些应用程序中,对象通常由 MVC 模型绑定器创建,需要特殊处理。在下面的部分中,我将解释每个删除行为的工作原理。


删除行为的选择可能会令人困惑,并且很难确定您需要哪一种行为,即使在随后的章节中看到了每一种行为的演示之后也是如此。如果你发现自己不确定从哪里开始,这是我的建议。

如果您正在处理必要关系,那么您应该从Cascade行为开始,但是要做一些测试,以确保单个删除操作不会在数据库中传播并删除比您预期的更多的数据。

如果您正在处理一个可选关系,那么如果您的数据库服务器支持SetNull行为,那么您应该从它开始,否则使用ClientSetNull行为。但是,如果您没有处理孤立数据的计划,那么应该使用Cascade行为。

应当避免使用Restrict行为,除非您正在建模具有无法使用其他行为处理的特定需求的数据库。Restrict行为允许您完全控制删除过程,但这可能比您预期的更难实现,而且很容易出错。


使用级联删除行为

DeleteBehavior.Cascade值配置数据库,以便在与其关联的主要实体被删除时,依赖实体被从数据库中移除。级联行为是最容易处理的,但是需要注意,因为您可以轻松地删除比预期更多的数据,因为数据库服务器将继续跟踪关系和删除数据,以确保数据库的完整性。如果您过于随意地应用级联行为,您会发现删除操作可能失去控制并产生深远的影响。

在清单22-5中,我向 context 类添加了一个 Fluent API 语句,为 Employee/SecondaryIdentity 关系选择级联行为。

注意:关系的删除行为仅能使用 Fluent API 指定,特性不支持此功能。

清单 22-5:Models 文件夹下的 AdvancedContext.cs 文件,配置删除行为

using Microsoft.EntityFrameworkCore;
using System;

namespace AdvancedApp.Models
{
    public class AdvancedContext : DbContext
    {
        public AdvancedContext(DbContextOptions<AdvancedContext> options)
            : base(options) { }

        public DbSet<Employee> Employees { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Employee>()
                .HasQueryFilter(e => !e.SoftDeleted);

            modelBuilder.Entity<Employee>().Ignore(e => e.Id);
            modelBuilder.Entity<Employee>()
                .HasKey(e => new { e.SSN, e.FirstName, e.FamilyName });

            modelBuilder.Entity<Employee>()
                .Property(e => e.Salary).HasColumnType("decimal(8,2)")
                .HasField("databaseSalary")
                .UsePropertyAccessMode(PropertyAccessMode.Field);
            //.IsConcurrencyToken();

            modelBuilder.Entity<Employee>().Property<DateTime>("LastUpdated")
                .HasDefaultValue(new DateTime(2000, 1, 1));

            modelBuilder.Entity<Employee>()
                .Property(e => e.RowVersion).IsRowVersion();

            modelBuilder.Entity<SecondaryIdentity>()
                .HasOne(s => s.PrimaryIdentity)
                .WithOne(e => e.OtherIdentity)
                .HasPrincipalKey<Employee>(e => new
                {
                    e.SSN,
                    e.FirstName,
                    e.FamilyName
                })
                .HasForeignKey<SecondaryIdentity>(s => new
                {
                    s.PrimarySSN,
                    s.PrimaryFirstName,
                    s.PrimaryFamilyName
                }).OnDelete(DeleteBehavior.Cascade); //代码修改处

            modelBuilder.Entity<SecondaryIdentity>()
                .Property(e => e.Name).HasMaxLength(100);
        }
    }
}

OnDelete方法用于配置关系的删除行为,并接收一个DeleteBehavior值作为它的参数。此方法用作定义两个类之间关系的方法调用链的一部分。

在 AdvancedApp 项目文件夹中运行清单22-6所示的命令,向数据库创建并应用更改删除行为迁移。

清单 22-6:创建并应用迁移

dotnet ef migrations add CascadeDelete
dotnet ef database update

要查看更改效果,使用dotnet run启动应用程序,并导航至 http://localhost:5000,单击某一个 Employee 对象的【Delete】按钮。Employee和与之关联的SecondaryIdentity对象将被从数据库删除,如图22-3所示。

提示:如果要删除的对象已用完,请单击【Create】按钮,填写字段,然后单击【Save】将新数据存储在数据库中。您选择的值并不重要,因为本章中的所有示例都只关注删除数据。

图22-3 使用级联删除行为

级联行为由数据库服务器实现,它自动从数据库中删除关联数据。您可以通过检查应用程序生成的日志信息看到这一点。当单击【Delete】按钮时,Entity Framework Core 向数据库发送以下 SQL:

...
DELETE FROM [Employees]
WHERE [SSN] = @p0 AND [FirstName] = @p1 AND [FamilyName] = @p2
    AND [RowVersion] = @p3;
...

Entity Framework Core 只需删除主要实体,并依靠数据库服务器处理删除关联数据,以保持数据库的完整性。

将外键设置为空

DeleteBehavior的两个值:SetNullClientSetNull,通过在数据库中保留依赖实体但将外键设置为null来响应删除主体实体。这会破坏两个对象之间的关系,从而可以删除主要实体,而不会破坏数据库的引用完整性。

警告:此行为仅可用于可选关系。在必要关系上使用此行为将产生异常。

SetNullClientSetNull行为的不同之处在于谁负责更改依赖实体。对于SetNull行为,数据库服务器将设置依赖实体的外键属性为空。对于ClientSetNull行为,Entity Framework Core 负责更新依赖实体。

并非所有数据库都支持SetNull行为 —— 尽管 SQL Server 支持 —— 但SetNull具有一致性的优点,因为ClientSetNull行为仅在 Entity Framework Core 意识到关联数据时才工作,这要么是因为它已作为查询的一部分加载,要么是因为 MVC 模型绑定器创建了一个包含其主键的对象。我在下面的章节中演示了使用这些行为的不同方式。

依赖数据库服务器更改外键

如果您的数据库服务器支持它,那么SetNull行为可以用于更新依赖实体,即使它们还没有被 Entity Framework Core 加载。在将外键设置为null的两种删除行为中,这是最容易处理的行为,因为您不必确保 Entity Framework Core 跟踪必须修改的对象。在清单22-7中,我更改OnDelete方法的参数为 Employee/SecondaryIdentity 关系选择了SetNull行为。

清单 22-7:Models 文件夹下的 AdvancedContext.cs 文件,更改删除行为

using Microsoft.EntityFrameworkCore;
using System;

namespace AdvancedApp.Models
{
    public class AdvancedContext : DbContext
    {
        public AdvancedContext(DbContextOptions<AdvancedContext> options)
            : base(options) { }

        public DbSet<Employee> Employees { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Employee>()
                .HasQueryFilter(e => !e.SoftDeleted);

            modelBuilder.Entity<Employee>().Ignore(e => e.Id);
            modelBuilder.Entity<Employee>()
                .HasKey(e => new { e.SSN, e.FirstName, e.FamilyName });

            modelBuilder.Entity<Employee>()
                .Property(e => e.Salary).HasColumnType("decimal(8,2)")
                .HasField("databaseSalary")
                .UsePropertyAccessMode(PropertyAccessMode.Field);
            //.IsConcurrencyToken();

            modelBuilder.Entity<Employee>().Property<DateTime>("LastUpdated")
                .HasDefaultValue(new DateTime(2000, 1, 1));

            modelBuilder.Entity<Employee>()
                .Property(e => e.RowVersion).IsRowVersion();

            modelBuilder.Entity<SecondaryIdentity>()
                .HasOne(s => s.PrimaryIdentity)
                .WithOne(e => e.OtherIdentity)
                .HasPrincipalKey<Employee>(e => new
                {
                    e.SSN,
                    e.FirstName,
                    e.FamilyName
                })
                .HasForeignKey<SecondaryIdentity>(s => new
                {
                    s.PrimarySSN,
                    s.PrimaryFirstName,
                    s.PrimaryFamilyName
                }).OnDelete(DeleteBehavior.SetNull); //更改此处

            modelBuilder.Entity<SecondaryIdentity>()
                .Property(e => e.Name).HasMaxLength(100);
        }
    }
}

在 AdvancedApp 项目文件夹下运行清单 22-8 所示的命令,创建一个新的迁移并应用到数据库。

清单 22-8:创建并应用数据库迁移

dotnet ef migrations add SetNullDelete
dotnet ef database update

要查看删除行为是如何工作的,使用dotnet run启动应用程序,导航至 http://localhost:5000,并单击某个【Delete】按钮。您将看到Employee对象从数据库移除,关联的SecondaryIdentity对象的外键属性被设置为空,如图22-4所示。

图22-4 使用 SetNull 行为

如果您检查应用程序生成的日志消息,将看到 Entity Framework Core 用于删除Employee对象的DELETE命令。

...
DELETE FROM [Employees]
WHERE [SSN] = @p0 AND [FirstName] = @p1 AND [FamilyName] = @p2
    AND [RowVersion] = @p3;
...

没有更新关联的SecondaryIdentity对象命令被发送,这条语句表示数据库服务器更新任意依赖实体的外键属性为空。

警告:由于SetNull行为不从数据库删除依赖实体,所以可能会得到孤立的数据。只有当应用程序可能重用数据库中的依赖实体时,才应当使用此行为。对于示例应用程序,数据库中现在有一个不与Employee关联的SecondaryIdentity对象,应用程序在创建新Employee时不提供使用现有对象的方法,从而使数据成为孤儿。孤立的数据可能会导致意外的问题,特别是当存在应用于原生键的唯一性约束时,这将阻止使用孤儿使用的相同键值创建新对象。

依赖 Entity Framework Core 更新外键

使用ClientSetNull行为,Entity Framework Core 将在删除主要实体时更新依赖实体的外键属性。如果您的数据库服务器不支持SetNull行为,这是非常有用的,尽管它需要在 ASP.NET Core MVC 应用程序中进行一些额外的工作,因为 Entity Framework Core 将只更新那些从数据库加载或手动添加以进行更改跟踪的对象。这意味着要么使用额外查询从数据库加载关联数据,要么在用户为启动删除操作而发送的 HTTP 请求中包含其他信息。

为配置ClientSetNull行为,我在 context 类中更改了传递给OnDelete方法的值,如清单 22-9 所示。

清单 22-9:Models 文件夹下的 AdvancedContext.cs 文件,更改删除行为

modelBuilder.Entity<SecondaryIdentity>()
    .HasOne(s => s.PrimaryIdentity)
    .WithOne(e => e.OtherIdentity)
    .HasPrincipalKey<Employee>(e => new
    {
        e.SSN,
        e.FirstName,
        e.FamilyName
    })
    .HasForeignKey<SecondaryIdentity>(s => new
    {
        s.PrimarySSN,
        s.PrimaryFirstName,
        s.PrimaryFamilyName
    }).OnDelete(DeleteBehavior.ClientSetNull); //更改此处

需要新的迁移配置数据库,在 AdvancedApp 文件夹运行清单22-10所示的命令,创建迁移并将之应用至数据库。

清单 22-10:创建并应用数据库迁移

dotnet ef migrations add ClientSetNullDelete
dotnet ef database update

如果您检查添加到 Migrations 文件夹的<timestamp>_ClientSetNullDelete.cs文件中的Up方法,您将看到删除行为已被改变,如下:

...
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_SecondaryIdentity_Employees_PrimarySSN_PrimaryFirstName_PrimaryFamilyName",
        table: "SecondaryIdentity");

    migrationBuilder.AddForeignKey(
        name: "FK_SecondaryIdentity_Employees_PrimarySSN_PrimaryFirstName_PrimaryFamilyName",
        table: "SecondaryIdentity",
        columns: new[] { "PrimarySSN", "PrimaryFirstName", "PrimaryFamilyName" },
        principalTable: "Employees",
        principalColumns: new[] { "SSN", "FirstName", "FamilyName" },
        onDelete: ReferentialAction.Restrict); //手动高亮
}
...

迁移将行为更改回Restrict,这样,在Employee对象被删除时,数据库服务器不会有任何动作。这是有意义的,因为ClientSetNull行为依赖于 Entity Framework Core 来更新外键值,而不是数据库服务器。

如果您使用dotnet run启动应用程序,并导航至 http://localhost:5000,并单击一个【Delete】按钮,将看到图22-5所示的错误信息。

图22-5 删除主要实体

如果选择ClientSetNull行为,但不为 Entity Framework Core 提供对与被删除对象关联的依赖实体的访问,则会发生这种情况。其效果相当于指定Restrict行为,因为数据库服务器不会对依赖实体进行任何更改,Entity Framework Core 也不知道它们。

使用查询载入关联数据

确保 Entity Framework Core 知道要删除的关联对象的一种方法是执行数据库查询来加载数据,如清单22-11所示。

清单 22-11:Controllers 文件夹下的 HomeController.cs 文件,查询关联数据

[HttpPost]
        public IActionResult Delete(Employee employee)
        {
            context.Set<SecondaryIdentity>().FirstOrDefault(id =>
                id.PrimarySSN == employee.SSN
                && id.PrimaryFirstName == employee.FirstName
                && id.PrimaryFamilyName == employee.FamilyName);

            context.Employees.Remove(employee);
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }

加载将要删除的数据就足够了,除了确保它被跟踪之外,我不需要对 Entity Framework Core 创建的对象做任何事情。

提示:我使用了Find方法,它立即执行查询。如果您的关联数据仅能通过创建了IQueryable<T>的 LINQ 查询访问,那么应该调用Load方法强制对查询进行计算。否则,Entity Framework Core 将不会执行查询,数据不会被加载,外键属性也不会设置为null

使用dotnet run启动应用程序,导航至 http://localhost:5000,并单击【Delete】按钮。与SetNull行为一样,Employee对象将从数据库中删除,关联的SecondaryIdentity对象的外键属性将设置为空,如图22-6所示。

图22-6 使用 ClientSetNull 行为

如果检查应用程序生成的日志消息,您将看到 Entity Framework Core 负责处理这两个对象。首先,您将看到以下命令,它将第SecondaryIdentity对象的外键值设置为null

...
UPDATE [SecondaryIdentity] SET [PrimaryFamilyName] = @p0, [PrimaryFirstName] = @p1,
    [PrimarySSN] = @p2
WHERE [Id] = @p3;
...

这确保可以删除Employee对象,而不会导致引用完整性问题。使用以下命令执行删除操作:

...
DELETE FROM [Employees]
WHERE [SSN] = @p4 AND [FirstName] = @p5 AND [FamilyName] = @p6
    AND [RowVersion] = @p7;
...

避免关联数据查询

实体框架核心只需要依赖实体的主键来执行更新。您可以避免清单22-11中的额外查询,方法是在针对Delete操作的 HTTP POST 请求中添加一个附加值,并使用它为 Entity Framework Core 提供它所需的信息。在清单22-12中,我为关联的SecondaryIdentity对象的主键的 Index 视图添加了一个元素。

清单 22-12:Views/Home 文件夹下的 Index.cshtml 文件,添加一个元素

@model IEnumerable<Employee>
@{
    ViewData["Title"] = "Advanced Features";
    Layout = "_Layout";
}
<h3 class="bg-info p-2 text-center text-white">Employees</h3>
<table class="table table-sm table-striped">
    <thead>
        <tr>
            <th>SSN</th>
            <th>First Name</th>
            <th>Family Name</th>
            <th>Salary</th>
            <th>Other Name</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr class="placeholder"><td colspan="7" class="text-center">No Data</td></tr>
        @foreach (Employee e in Model)
        {
    <tr>
        <td>@e.SSN</td>
        <td>@e.FirstName</td>
        <td>@e.FamilyName</td>
        <td>@e.Salary</td>
        <td>@(e.OtherIdentity?.Name ?? "(None)")</td>
        <td class="text-right">
            <form>
                <input type="hidden" name="SSN" value="@e.SSN" />
                <input type="hidden" name="Firstname" value="@e.FirstName" />
                <input type="hidden" name="FamilyName"
                       value="@e.FamilyName" />
                <input type="hidden" name="RowVersion"
                       asp-for="@e.RowVersion" />
                <input type="hidden" name="OtherIdentity.Id" @*新增代码*@
                       value="@e.OtherIdentity.Id" />
                <button type="submit" asp-action="Delete" formmethod="post"
                        class="btn btn-sm btn-danger">
                    Delete
                </button>
                <button type="submit" asp-action="Edit" formmethod="get"
                        class="btn btn-sm btn-primary">
                    Edit
                </button>
            </form>
        </td>
    </tr>
        }
    </tbody>
</table>
@await  Html.PartialAsync("SecondaryIdentities")
<div class="text-center">
    <a asp-action="Edit" class="btn btn-primary">Create</a>
</div>

在清单22-13中,我已经注释掉了Delete action 方法中的查询。HTTP POST 请求中包含的新值允许 MVC 模型绑定器创建SecondaryIdentity对象,Entity Framework Core 将使用该对象在执行删除操作之前更新数据库。

清单 22-13:Controllers 文件夹下的 HomeController.cs 文件,禁用查询

...
[HttpPost]
public IActionResult Delete(Employee employee)
{
    //context.Set<SecondaryIdentity>().FirstOrDefault(id =>
    //    id.PrimarySSN == employee.SSN
    //    && id.PrimaryFirstName == employee.FirstName
    //    && id.PrimaryFamilyName == employee.FamilyName);

    context.Employees.Remove(employee);
    context.SaveChanges();
    return RedirectToAction(nameof(Index));
}
...

使用dotnet run启动应用程序,导航至 http://localhost:5000,单击【Delete】按钮。关联的SecondaryIdentity对象的外键属性将被设置为null,和之前的示例一样,但这次无需在Delete action 方法中执行附加查询。

控制删除操作

Restrict告诉 Entity Framework Core 和数据库服务器不对依赖实体做任何更改。如果选择此行为,则负责确保可以在不导致错误的情况下执行删除操作。在清单22-14中,我在 context 类中选择了删除行为。

清单 22-14:Models 文件夹下的 AdvancedContext.cs 文件,选择 Restrict 行为

...
modelBuilder.Entity<SecondaryIdentity>()
    .HasOne(s => s.PrimaryIdentity)
    .WithOne(e => e.OtherIdentity)
    .HasPrincipalKey<Employee>(e => new
    {
        e.SSN,
        e.FirstName,
        e.FamilyName
    })
    .HasForeignKey<SecondaryIdentity>(s => new
    {
        s.PrimarySSN,
        s.PrimaryFirstName,
        s.PrimaryFamilyName
    }).OnDelete(DeleteBehavior.Restrict);
...

在 AdvancedApp 项目文件夹下运行清单 22-15 所示的命令,创建一个新的迁移,并应用到数据库。

清单 22-15:创建并应用数据库迁移

dotnet ef migrations add RestrictDelete
dotnet ef database update

重建级联行为

如果要删除关联数据,则必须查询数据库中要删除的对象,或者确保 HTTP POST 请求中有足够的数据来创建一个允许 Entity Framework Core 执行删除操作的对象。

在清单22-12中,我向 Index.cshtml 视图添加了一个隐藏的input元素,该视图包括请求中删除EmployeeSecondaryIdentity对象的主键值。在清单22-16中,我使用这个值告诉 Entity Framework Core 在删除操作中包含关联对象。

清单 22-16:Controllers 文件夹下的 HomeController.cs 文件,删除关联数据

[HttpPost]
public IActionResult Delete(Employee employee)
{
    if (employee.OtherIdentity != null)
    {
        context.Set<SecondaryIdentity>().Remove(employee.OtherIdentity);
    }

    context.Employees.Remove(employee);
    context.SaveChanges();
    return RedirectToAction(nameof(Index));
}

使用dotnet run启动应用程序,导航至 http://localhost:5000,单击【Delete】按钮(您可能需要创建新的对象以便有东西进行删除)。检查应用程序生成的日志消息,您将看到两个从数据库中删除数据的操作。第一个移除SecondaryIdentity对象。

...
DELETE FROM [SecondaryIdentity]
WHERE [Id] = @p0;
...

删除依赖实体将为第二次操作扫清道路,第二次操作将移除主要实体。

...
DELETE FROM [Employees]
WHERE [SSN] = @p1 AND [FirstName] = @p2 AND [FamilyName] = @p3
    AND [RowVersion] = @p4;
...

重建 SetNull 行为

如果希望将外键属性设置为null,则可以使用Attach方法将 MVC 模型绑定器创建的对象置于 Entity Framework Core 更改跟踪之下,然后设置外键属性的值,如清单22-17所示。

注意:这只适用于可选的关系。如果尝试在必需关系中将外键属性设置为null,则会收到错误。

清单 22-17:Controllers 文件夹下的 HomeController.cs 文件,设置外键属性

...
[HttpPost]
public IActionResult Delete(Employee employee)
{
    if (employee.OtherIdentity != null)
    {
        SecondaryIdentity identity =
            context.Set<SecondaryIdentity>().Find(employee.OtherIdentity.Id);
        identity.PrimarySSN = null;
        identity.PrimaryFirstName = null;
        identity.PrimaryFamilyName = null;
    }

    context.Employees.Remove(employee);
    context.SaveChanges();
    return RedirectToAction(nameof(Index));
}
...

我查询数据库以获取当前外键值,将属性设置为null,并将Employee对象上的OtherIdentity导航属性设置为null。当 Entity Framework Core 更新数据库时,它发送UPDATEDELETE命令,就像ClientSetNull行为所使用的命令一样。

恢复软删除功能

我将通过恢复软删除功能来完成本章,并将永久删除功能移到单独的Delete控制器中。

在清单22-18中,我修改了 Home 控制器,以便Delete action 执行软删除。我还更改了Index action 中的关联数据的查询,以便可以通过Employee导航属性访问SecondaryIdentity对象。这将确保查询受查询过滤器的约束,以便不会显示外键属性为空的SecondaryIdentity对象。

清单 22-18:Controllers 文件夹下的 HomeController.cs 文件,更新 Actions

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System;
using System.Collections.Generic;

namespace AdvancedApp.Controllers
{
    public class HomeController : Controller
    {
        private AdvancedContext context;
        public HomeController(AdvancedContext ctx) => context = ctx;

        public IActionResult Index()
        {
            IEnumerable<Employee> data = context.Employees
                .Include(e => e.OtherIdentity)
                .OrderByDescending(e => e.LastUpdated)
                .ToArray();
            ViewBag.Secondaries = data.Select(e => e.OtherIdentity);
            return View(data);
        }

        public IActionResult Edit(string SSN, string firstName, string familyName)
        {
            return View(string.IsNullOrWhiteSpace(SSN)
                ? new Employee() : context.Employees.Include(e => e.OtherIdentity)
                    .First(e => e.SSN == SSN
                        && e.FirstName == firstName
                        && e.FamilyName == familyName));
        }

        [HttpPost]
        public IActionResult Update(Employee employee)
        {
            if (context.Employees.Count(e => e.SSN == employee.SSN
            && e.FirstName == employee.FirstName
            && e.FamilyName == employee.FamilyName) == 0)
            {
                context.Add(employee);
            }
            else
            {
                Employee e = new Employee
                {
                    SSN = employee.SSN,
                    FirstName = employee.FirstName,
                    FamilyName = employee.FamilyName,
                    RowVersion = employee.RowVersion
                };
                context.Employees.Attach(e);
                e.Salary = employee.Salary;
                e.LastUpdated = DateTime.Now;
            }
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }

        [HttpPost]
        public IActionResult Delete(Employee employee)
        {
            context.Employees.Attach(employee);
            employee.SoftDeleted = true;
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }
    }
}

查询过滤器仅适用于对其应用到的类的查询,这意味着如果使用从关系的不同端开始的查询,则可能会得到不一致的结果。在Index action 中,我更改了对SecondaryIdentity对象的查询,以便它从查询Employee对象中选择结果的子集。这确保只向用户显示与未被软删除的Employee对象关联的SecondaryIdentity对象。

提示:为了确保只执行一个查询,我使用ToArray方法强制执行查询。如果没有此方法,则当视图枚举EmployeeSecondaryIdentity对象时会出现重复查询。

接下来,我在Delete控制器使用的 Index 视图中添加了按钮,以便可以单独或批量地从数据库中删除软删除对象,如清单22-19所示。

清单 22-19:Views/Delete 文件夹下的 Index.cshtml 文件,添加元素

@model IEnumerable<Employee>
@{
    ViewData["Title"] = "Advanced Features";
    Layout = "_Layout";
}
<h3 class="bg-info p-2 text-center text-white">Deleted Employees</h3>
<table class="table table-sm table-striped">
    <thead>
        <tr>
            <th>SSN</th>
            <th>First Name</th>
            <th>Family Name</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr class="placeholder"><td colspan="4" class="text-center">No Data</td></tr>
        @foreach (Employee e in Model)
        {
            <tr>
                <td>@e.SSN</td>
                <td>@e.FirstName</td>
                <td>@e.FamilyName</td>
                <td class="text-right">
                    <form method="post">
                        <input type="hidden" name="SSN" value="@e.SSN" />
                        <input type="hidden" name="FirstName" value="@e.FirstName" />
                        <input type="hidden" name="FamilyName"
                               value="@e.FamilyName" />
                        <input type="hidden" name="RowVersion"
                               asp-for="@e.RowVersion" />
                        <input type="hidden" name="OtherIdentity.Id"
                               value="@e.OtherIdentity.Id" />
                        <button asp-action="Restore" class="btn btn-sm btn-success">
                            Restore
                        </button>
                        <button asp-action="Delete" class="btn btn-sm btn-danger">
                            Delete
                        </button>
                    </form>
                </td>
            </tr>
        }
    </tbody>
</table>
<div class="text-center">
    <form method="post" asp-action="DeleteAll">
        <button type="submit" class="btn btn-danger">Delete All</button>
    </form>
</div>

为完成功能,我将清单22-20所示的 action 添加到Delete控制器中,对应于清单22-19中添加的元素。

清单 22-20:Controllers 文件夹下的 DeleteController.cs 文件,添加 action

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;

namespace AdvancedApp.Controllers
{
    public class DeleteController : Controller
    {
        private AdvancedContext context;
        public DeleteController(AdvancedContext ctx) => context = ctx;

        public IActionResult Index()
        {
            return View(context.Employees.Where(e => e.SoftDeleted)
            .Include(e => e.OtherIdentity).IgnoreQueryFilters());
        }

        [HttpPost]
        public IActionResult Restore(Employee employee)
        {
            context.Employees.IgnoreQueryFilters()
            .First(e => e.SSN == employee.SSN
            && e.FirstName == employee.FirstName
            && e.FamilyName == employee.FamilyName).SoftDeleted = false;
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }

        [HttpPost]
        public IActionResult Delete(Employee e)
        {
            if (e.OtherIdentity != null)
            {
                context.Remove(e.OtherIdentity);
            }
            context.Employees.Remove(e);
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }

        [HttpPost]
        public IActionResult DeleteAll()
        {
            IEnumerable<Employee> data = context.Employees
            .IgnoreQueryFilters()
            .Include(e => e.OtherIdentity)
            .Where(e => e.SoftDeleted).ToArray();
            context.RemoveRange(data.Select(e => e.OtherIdentity));
            context.RemoveRange(data);
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }
    }
}

action 方法必须确保与Employee对象相关的SecondaryIdentity对象也从数据库中删除,因为数据模型配置了Restrict删除行为。要测试 软删除/硬删除 功能,请使用dotne trun启动应用程序,导航到 http://localhost:5000,然后单击一个或多个Employee对象的【Delete】按钮。导航到 http://localhost:5000/delete,您可以使用图22-7中所示的按钮恢复对象,永久删除单个软删除对象,或删除所有软删除对象。

图22-7 恢复和完成软删除功能

总结

在本章中,我描述了 Entity Framework Core 支持的删除数据的不同行为。我展示了CascadeSetNull/ClientSetNull行为之间的区别,以及如何使用Restrict行为来控制删除过程。我通过还原软删除功能并添加对从软删除状态中永久删除对象的支持来完成本章的工作。在下一章中,我将描述 Entity Framework Core 为使用数据库服务器提供的高级功能所提供的特性。

;

© 2018 - IOT小分队文章发布系统 v0.3